package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.*;

@Slf4j
@Component
public class CacheClient {

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    private final StringRedisTemplate stringRedisTemplate;
    //  基于构造函数注入
    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public void set(String key, Object value, Long time, TimeUnit timeUnit) {
//      需要把Object类型的value转换成JSON类型的字符串
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, timeUnit);
    }

    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
        RedisData redisData = new RedisData();
        redisData.setData(value);
//      对于传入进来的时间单位可能不一致，统一转换为秒
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
//      需要把Object类型的value转换成JSON类型的字符串
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    /*    对于返回的类型并不知道，所以需要由传入的参数指定，定义返回类型为R，并且查询的id也并不一定是Long类型，利用泛型指定
           对于数据库的查询逻辑，通过函数式变成来告诉该工具类
           Function<ID, R> 表示有参数有返回值的函数式编程，第一位是参数类型，第二位是返回值类型
           并且还需要指定过期的时间

     */
    public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback,Long time, TimeUnit unit ) {
//        1 从redis中查询id对应的店铺缓存是否存在
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(json)) {
            //        2 缓存存在 直接返回，返回的类型为泛型，利用JSON工具将JSON字符串转为泛型的类型
            R r = JSONUtil.toBean(json, type);
            return r;
        }
//        判断在redis中是否是空值,如果不为空值，且也没有值，说明是缓存空对象，返回错误
        if (json != null) {
            return null;
        }

//        3 缓存不存在 根据id查询数据库，对于这一段逻辑，由于不同表的查询规则不知道，所以直接交给调用者
        R r = dbFallback.apply(id);
//        4 数据库中不存在，直接返回
        if (r == null) {
//            4.1 将空值写入到redis
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
//        5 数据库中存在，写入到redis中
//        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(r), time, unit);
        this.set(key, r, time, unit); // 直接调用上面编写的方法
//        6 返回
        return r;
    }

    private boolean trylock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "lock", 10, TimeUnit.SECONDS);
//        Boolean是一个封装类型，其值可能会有true，false，和null，
//        当redis操作出现异常时，可能会导致空指针异常，所以不能直接返回flag
        return BooleanUtil.isTrue(flag);
    }
    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }

    public <R, ID> R queryWithMutext(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback,Long time, TimeUnit unit) {
//        1 从redis中查询id对应的店铺缓存是否存在
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(json)) {
            //        2 缓存存在 直接返回
            R r = JSONUtil.toBean(json, type);
            return r;
        }
//        判断在redis中是否是空值,如果不为空值，且也没有值，说明是缓存空对象，返回错误
        if (json != null) {
            return null;
        }

//        3 redis为空，实现缓存重建
//        3.1 获取互斥锁
        String lockKey = "lock:shop:" + id; // 每个店铺都有一个自己的锁
        R r = null;
        try {
            boolean islock = trylock(lockKey);
//        3.2 判断是否获取成功
            if (!islock) {
                //        3.3 失败，进入休眠模式
                Thread.sleep(50);
                return queryWithMutext(keyPrefix, id, type, dbFallback, time, unit); // 此处return 一个递归，相当于开启了一个新的线程重新执行一遍
            }
//
//        3.4 成功，根据id查询数据库
            r = dbFallback.apply(id);
//            模拟重建延迟
            Thread.sleep(200);

//        4 数据库中不存在，直接返回
            if (r == null) {
                //            4.1 将空值写入到redis
                stringRedisTemplate.opsForValue().set(key, "",CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
//        5 数据库中存在，写入到redis中
            this.setWithLogicalExpire(key, r, time, unit);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //        6. 释放互斥锁
            unlock(lockKey);
        }
//        7 返回
        return r;
    }

    public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback,Long time, TimeUnit unit) {
//        1 从redis中查询id对应的店铺缓存是否存在
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
//      2. 判断缓存是否存在
        if (StrUtil.isBlank(json)) {
            //        3 缓存不存在 直接返回null，一般不存在，除非这个活动已下架,
            log.debug("需要数据预热");
            return null;
        }
//        4 缓存存在 将JSON反序列化为对象
        RedisData redisdata = JSONUtil.toBean(json, RedisData.class); // 转为RedisData对象
        JSONObject data = (JSONObject)redisdata.getData(); // 将其中的data由Object对象转为JSONObject对象
        R r = JSONUtil.toBean(data, type); // 转为目标对象类型
        LocalDateTime expireTime = redisdata.getExpireTime();
//        System.out.println("expireTime : " + expireTime);
//        5 判断当前数据是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
//        5.1 未过期，直接返回店铺信息
//            log.debug("未过期，直接返回当前数据");
            return r;
        }

//        5.2 已过期，需要缓存重建
//        6 缓存重建
//        6.1 获取互斥锁
        String lockKey = "lock" + keyPrefix + id;
        boolean islock = trylock(lockKey);
//        6.2 判断互斥锁是否获取成功
        if (islock) {
//        6.3 获取成功，开启新线程
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
//                    执行重建缓存
//                    查询数据库
                    R r1 = dbFallback.apply(id);

//                    写入redis
//                    log.debug("key: " + key + " r1 " + r1 + "time: " + time + " unit: " + unit);
                    this.setWithLogicalExpire(key, r1, time, unit);

                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                finally {
//                    释放锁
                    unlock(lockKey);
                }
            });
        }

//        6.4 返回旧数据

        return r;
    }



}
